﻿/*
* Copyright (c) 2010-2012 Tesla Engine Group
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
* 
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
* 
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

using Tesla.Math;

namespace Tesla.Graphics {
    /// <summary>
    /// Struct representing a set of planes that comprise a camera's bounding frustum.
    /// </summary>
    internal struct BoundingFrustum {
        private Plane[] _planes;
        private Vector3[] _corners;
        private bool _cornersDirty;

        /// <summary>
        /// Left plane (index 0)
        /// </summary>
        public Plane Left {
            get {
                return _planes[0];
            }
        }

        /// <summary>
        /// Right plane (index 1)
        /// </summary>
        public Plane Right {
            get {
                return _planes[1];
            }
        }

        /// <summary>
        /// Top plane (index 2)
        /// </summary>
        public Plane Top {
            get {
                return _planes[2];
            }
        }

        /// <summary>
        /// Bottom plane (index 3)
        /// </summary>
        public Plane Bottom {
            get {
                return _planes[3];
            }
        }

        /// <summary>
        /// Near plane (index 4)
        /// </summary>
        public Plane Near {
            get {
                return _planes[4];
            }
        }

        /// <summary>
        /// Far plane (index 5)
        /// </summary>
        public Plane Far {
            get {
                return _planes[5];
            }
        }

        /// <summary>
        /// Creates a new frustum with the given view-projection matrix.
        /// </summary>
        /// <param name="viewProjMatrix">View x Projection matrix</param>
        public BoundingFrustum(Matrix viewProjMatrix) {
            _planes = new Plane[6];
            _corners = new Vector3[8];
            _cornersDirty = true;
            SetMatrix(ref viewProjMatrix);
        }

        /// <summary>
        /// Sets the frustum planes to correspond to the new view-projection matrix.
        /// </summary>
        /// <param name="viewProjMatrix">View x Projection matrix</param>
        public void SetMatrix(ref Matrix viewProjMatrix) {
            Plane left = new Plane(viewProjMatrix.M14 + viewProjMatrix.M11,
                              viewProjMatrix.M24 + viewProjMatrix.M21,
                              viewProjMatrix.M34 + viewProjMatrix.M31,
                              viewProjMatrix.M44 + viewProjMatrix.M41);
            left.Normalize();
            _planes[0] = left;

            Plane right = new Plane(viewProjMatrix.M14 - viewProjMatrix.M11,
                               viewProjMatrix.M24 - viewProjMatrix.M21,
                               viewProjMatrix.M34 - viewProjMatrix.M31,
                               viewProjMatrix.M44 - viewProjMatrix.M41);
            right.Normalize();
            _planes[1] = right;

            Plane top = new Plane(viewProjMatrix.M14 - viewProjMatrix.M12,
                             viewProjMatrix.M24 - viewProjMatrix.M22,
                             viewProjMatrix.M34 - viewProjMatrix.M32,
                             viewProjMatrix.M44 - viewProjMatrix.M42);
            top.Normalize();
            _planes[2] = top;

            Plane bottom = new Plane(viewProjMatrix.M14 + viewProjMatrix.M12,
                                viewProjMatrix.M24 + viewProjMatrix.M22,
                                viewProjMatrix.M34 + viewProjMatrix.M32,
                                viewProjMatrix.M44 + viewProjMatrix.M42);
            bottom.Normalize();
            _planes[3] = bottom;

            Plane near = new Plane(viewProjMatrix.M13, viewProjMatrix.M23, viewProjMatrix.M33, viewProjMatrix.M43);
            near.Normalize();
            _planes[4] = near;

            Plane far = new Plane(viewProjMatrix.M14 - viewProjMatrix.M13,
                             viewProjMatrix.M24 - viewProjMatrix.M23,
                             viewProjMatrix.M34 - viewProjMatrix.M33,
                             viewProjMatrix.M44 - viewProjMatrix.M43);
            far.Normalize();
            _planes[5] = far;

            _cornersDirty = true;
        }

        /// <summary>
        /// Get a frustum plane by its index.
        /// </summary>
        /// <param name="index">Index of plane</param>
        /// <param name="plane">Plane result to store</param>
        public void GetPlane(ref int index, out Plane plane) {
            plane = _planes[index];
        }

        /// <summary>
        /// Fills the provided array with the 8 positions representing the corners of the bounding frustum.
        /// The array must have the size of 8.
        /// </summary>
        /// <param name="array">Array to fill</param>
        public void GetCorners(Vector3[] array) {
            if(_cornersDirty) {
                ComputeCorners();
                _cornersDirty = false;
            }

            _corners.CopyTo(array, 0);
        }

        /// <summary>
        /// Returns an array of 8 positions, representing the corners of the bounding frustum.
        /// </summary>
        /// <returns></returns>
        public Vector3[] GetCorners() {
            if(_cornersDirty) {
                ComputeCorners();
                _cornersDirty = false;
            }
            return (Vector3[]) _corners.Clone();
        }

        private void ComputeCorners() {
            //Left-Top
            Ray ray;
            ComputeRay(ref _planes[0], ref _planes[2], out ray);
            ComputeRayIntersection(ref _planes[4], ref ray, 0);
            ComputeRayIntersection(ref _planes[5], ref ray, 3);

            //Left-Bottom
            ComputeRay(ref _planes[3], ref _planes[0], out ray);
            ComputeRayIntersection(ref _planes[4], ref ray, 1);
            ComputeRayIntersection(ref _planes[5], ref ray, 2);

            //Right-Top
            ComputeRay(ref _planes[2], ref _planes[1], out ray);
            ComputeRayIntersection(ref _planes[4], ref ray, 4);
            ComputeRayIntersection(ref _planes[5], ref ray, 7);

            //Right-Bottom
            ComputeRay(ref _planes[1], ref _planes[3], out ray);
            ComputeRayIntersection(ref _planes[4], ref ray, 5);
            ComputeRayIntersection(ref _planes[5], ref ray, 6);
        }

        private static void ComputeRay(ref Plane p1, ref Plane p2, out Ray ray) {
            Vector3 dir;
            Vector3.Cross(ref p1.Normal, ref p2.Normal, out dir);
            float invLengthSquared = 1.0f / dir.LengthSquared();
            Vector3 origin = Vector3.Cross((-p1.D * p2.Normal) + (p2.D * p1.Normal), dir) * invLengthSquared;

            ray.Origin = origin;
            ray.Direction = dir;
        }

        private void ComputeRayIntersection(ref Plane p, ref Ray ray, int cornerIndex) {
            float NPDot;
            Vector3.Dot(ref p.Normal, ref ray.Origin, out NPDot);
            float NDirDot;
            Vector3.Dot(ref p.Normal, ref ray.Direction, out NDirDot);

            float scale = (-p.D - NPDot) / NDirDot;

            _corners[cornerIndex] = ray.Origin + (ray.Direction * scale);
        }
    }
}
